package dk.au.cs.peterbn.spoonprocessing;

import dk.au.cs.peterbn.annotations.Member;
import dk.au.cs.peterbn.annotations.Members;
import dk.au.cs.peterbn.annotations.Pattern;
import dk.au.cs.peterbn.annotations.Patterns;
import dk.au.cs.peterbn.patterns.CallSite;
import dk.au.cs.peterbn.patterns.DesignPattern;
import dk.au.cs.peterbn.patterns.MemberRole;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.processing.PatternInstance;
import dk.au.cs.peterbn.processing.ProcessingUtil;
import spoon.processing.AbstractManualProcessor;
import spoon.processing.Severity;
import spoon.reflect.declaration.*;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtReference;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtStackScanner;
import spoon.reflect.visitor.QueryVisitor;
import spoon.reflect.visitor.filter.AbstractFilter;

import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: peter
 * Date: 10/10/11
 * Time: 11:40 AM
 * To change this template use File | Settings | File Templates.
 */
public class CallSiteProcessor extends AbstractManualProcessor {

    public static CtTypeReference<Pattern> PATTERN_REF;
    public static CtTypeReference<Patterns> PATTERNS_REF;
    public static CtTypeReference<Member> MEMBER_REF;
    public static CtTypeReference<Members> MEMBERS_REF;
    private ProcessingUtil util;

    @Override
    public void init() {
        if (util == null) {
            throw new RuntimeException("Running without processingUtil");
        }
        super.init();
        PATTERN_REF = getFactory().Type().createReference(Pattern.class);
        MEMBER_REF = getFactory().Type().createReference(Member.class);
        MEMBERS_REF = getFactory().Type().createReference(Members.class);
        PATTERNS_REF = getFactory().Type().createReference(Patterns.class);
    }

    public void setUtil(ProcessingUtil util) {
        this.util = util;
    }

    @Override
    public void process() {
        //Stage 1: Get types with pattern annotations
        QueryVisitor<CtType<?>> typeQueryVisitor = new QueryVisitor<CtType<?>>(new AbstractFilter<CtType<?>>(CtType.class) {
            @Override
            public boolean matches(CtType<?> element) {
                return util.getAllPatternAnnotations(element).size() > 0;
            }
        });
        List<CtSimpleType<?>> all = getFactory().Type().getAll(true);
        typeQueryVisitor.scan(all);
        Set<CtSimpleType<?>> withPatterns = new HashSet<CtSimpleType<?>>(typeQueryVisitor.getResult());
        if (withPatterns.isEmpty()) {
            System.out.println("Didn't find any types with pattern annotations. Aborting");
            return;
        }
        //Stage 2: Gather all members with annotations annotations
        QueryVisitor<CtMethod<?>> methodQueryVisitor = new QueryVisitor<CtMethod<?>>(new AbstractFilter<CtMethod<?>>(CtMethod.class) {
            @Override
            public boolean matches(CtMethod element) {
                return util.getAllDeclaredMemberAnnotations(element.getReference()).size() > 0;
            }
        });
        methodQueryVisitor.scan(withPatterns);
        Set<CtMethod<?>> roledMethods = new HashSet<CtMethod<?>>(methodQueryVisitor.getResult());
        if (roledMethods.isEmpty()) {
            System.out.println("Didn't find any methods with member annotations. Aborting");
            return;
        }

        //Stage 2.1: Build a map from references to methods
        final Map<CtExecutableReference<?>, CtMethod<?>> methodRefs = new HashMap<CtExecutableReference<?>, CtMethod<?>>();
        for (CtMethod<?> ctMethod : roledMethods) {
            CtExecutableReference<?> reference = ctMethod.getReference();
            methodRefs.put(reference, ctMethod);
        }

        //Stage 3: Go through the whole compilation unit, and check that methods are called where, and only where, they are supposed to be called
        final Map<CtMethod, Set<CtCallSite>> referencingTypes = new HashMap<CtMethod, Set<CtCallSite>>();
        MyCtStackScanner scanner = new MyCtStackScanner(methodRefs, referencingTypes);
        scanner.scan(getFactory().Type().getAll(false)); //Scanning takes care of nested types (i hope)
        //Now, the referencingTypes should have been built and must be checked
        for (CtMethod ctMethod : roledMethods) {
            Set<CtCallSite> actualCallSites = referencingTypes.get(ctMethod);
            //Get the method roles
            List<CtAnnotation<Member>> memberRoles = getMemberRoles(ctMethod.getReference());
            //For each role, check that it is being called in the required places
            for (CtAnnotation<Member> memberAnnotation : memberRoles) {
                String memberAnnotationId = Member.Fields.val(Member.Fields.ID, memberAnnotation);
                PatternInstance patternInstance = util.getPatternInstance(memberAnnotationId);
                if (patternInstance == null) {
                    continue; //There might be errors in id declarations
                }
                DesignPattern pattern = patternInstance.getName();
                MemberRole role = pattern.getMember(Member.Fields.val(Member.Fields.ROLE, memberAnnotation));
                if (role == null) {
                    continue; //There might be wrongly declared member roles
                }
                Set<CallSite> formalCallSites = role.getCallSites();
                for (CallSite formalCallSite : formalCallSites) {
                    if (formalCallSite.getRestriction() == CallSite.Restriction.MANDATORY || formalCallSite.getRestriction() == CallSite.Restriction.ONLY) {
                        boolean callSitefound = false;
                        TypeRole formalTypeRole = formalCallSite.getType();
                        MemberRole formalMemberRole = formalCallSite.getMember();
                        //Go over the actual callsites and check if it is present
                        if (actualCallSites != null) {
                            for (CtCallSite actualCallSite : actualCallSites) {
                                CtType<?> containingType = actualCallSite.getContainingType();
                                CtExecutable containingExecutable = actualCallSite.getContainingExecutable();
                                Set<CtAnnotation<Pattern>> containingPatternAnnotations = util.getAllPatternAnnotations(containingType);
                                boolean isCorrectContainingType = false;
                                for (CtAnnotation<Pattern> containingPatternAnnotation : containingPatternAnnotations) {
                                    String containingId = Pattern.Fields.val(Pattern.Fields.ID, containingPatternAnnotation);
                                    boolean correctId = containingId.equals(memberAnnotationId);
                                    boolean correctPatternName = Pattern.Fields.val(Pattern.Fields.NAME, containingPatternAnnotation).equals(pattern.getIdentifier());
                                    String containingTypeRoleName = Pattern.Fields.val(Pattern.Fields.ROLE, containingPatternAnnotation);
                                    if (correctId
                                            && correctPatternName
                                            && containingTypeRoleName.equals(formalTypeRole.getName())) {
                                        isCorrectContainingType = true;
                                    }

                                    if (!isCorrectContainingType && correctId && correctPatternName) {
                                        Queue<TypeRole> parentRoleQueue = new LinkedList<TypeRole>();
                                        TypeRole containingTypeRole = pattern.getTypeRole(containingTypeRoleName);
                                        parentRoleQueue.add(containingTypeRole);
                                        while (!isCorrectContainingType && (containingTypeRole = parentRoleQueue.poll()) != null) {
                                            parentRoleQueue.addAll(containingTypeRole.getParentRoles());
                                            isCorrectContainingType |= containingTypeRole.getName().equals(formalTypeRole.getName());
                                        }

                                    }
                                }

                                boolean isCorrectContainingMethod = formalMemberRole == null;
                                if (!isCorrectContainingMethod) {
                                    List<CtAnnotation<Member>> memberAnnotations = getMemberRoles(containingExecutable.getReference());
                                    for (CtAnnotation<Member> annotation : memberAnnotations) {
                                        if (Member.Fields.val(Member.Fields.ID, annotation).equals(memberAnnotationId)
                                                && Member.Fields.val(Member.Fields.ROLE, annotation).equals(formalMemberRole.getName())) {
                                            isCorrectContainingMethod = true;
                                        }
                                    }
                                }

                                callSitefound |= isCorrectContainingType && isCorrectContainingMethod;
                            }
                        }

                        if (!callSitefound) {
                            getEnvironment().report(CallSiteProcessor.this, Severity.ERROR, ctMethod,
                                    String.format("Method not called at required callsite %s(id=\"%s\") %s%s",
                                            pattern.getIdentifier(),
                                            memberAnnotationId,
                                            formalCallSite.getType().getName(),
                                            (formalCallSite.getMember() != null ? "#" + formalCallSite.getMember().getName() : "")));
                        }
                    }

                }
            }
        }

    }

    public <K, V> void mapPut(Map<K, Set<V>> map, K key, V value) {
        Set<V> vs = map.get(key);
        if (vs == null) {
            vs = new HashSet<V>();
            map.put(key, vs);
        }
        vs.add(value);
    }

    private List<CtAnnotation<Member>> getMemberRoles(CtExecutableReference<?> ctExecutableReference) {
        List<CtAnnotation<Member>> memberRoles = new ArrayList<CtAnnotation<Member>>();
        if (!ctExecutableReference.isConstructor()) {
            CtTypeReference declaringType = ctExecutableReference.getDeclaringType();
            getMemberRolesRecursive(memberRoles, ctExecutableReference, declaringType);
        }
        return memberRoles;
    }

    private void getMemberRolesRecursive(List<CtAnnotation<Member>> memberRoles, CtExecutableReference<?> ctExecutableReference, CtTypeReference<?> declaringType) {
        for (CtExecutableReference<?> executableReference : declaringType.getDeclaredExecutables()) {
            if (ctExecutableReference.isOverriding(executableReference)) {
                memberRoles.addAll(util.getAllDeclaredMemberAnnotations(executableReference));
            }
        }
        Set<CtTypeReference<?>> superInterfaces = declaringType.getSuperInterfaces();
        for (CtTypeReference<?> superInterface : superInterfaces) {
            getMemberRolesRecursive(memberRoles, ctExecutableReference, superInterface);
        }

        CtTypeReference<?> superclass = declaringType.getSuperclass();
        if (superclass != null) {
            getMemberRolesRecursive(memberRoles, ctExecutableReference, superclass);
        }
    }

    /**
     * This is the class that checks for illegal callsites
     */
    private class MyCtStackScanner extends CtStackScanner {
        private final Map<CtExecutableReference<?>, CtMethod<?>> methodRefs;
        private final Map<CtMethod, Set<CtCallSite>> referencingTypes;

        public MyCtStackScanner(Map<CtExecutableReference<?>, CtMethod<?>> methodRefs,
                                Map<CtMethod, Set<CtCallSite>> referencingTypes) {
            this.methodRefs = methodRefs;
            this.referencingTypes = referencingTypes;
        }

        @Override
        protected void enterReference(CtReference e) {
            super.enterReference(e);
            if (e instanceof CtExecutableReference<?>) {
                CtExecutableReference<?> ctExecutableReference = (CtExecutableReference<?>) e;
                List<CtAnnotation<Member>> memberRoles = getMemberRoles(ctExecutableReference);

                //Scan outwards until the referencing type is found.
                CtExecutable containingExecutable = null;
                CtType<?> containingType = null;
                ListIterator<CtElement> stackIt = elementStack.listIterator();
                while (stackIt.hasNext()) stackIt.next(); //Search to the end of the stack (the top)
                while (stackIt.hasPrevious()) { //Traverse downwards
                    CtElement next = stackIt.previous();
                    if (next instanceof CtExecutable) {
                        containingExecutable = (CtExecutable) next;
                    }
                    if (next instanceof CtType<?>) {
                        containingType = (CtType<?>) next;
                        break; //Stop when reaching a type;
                    }
                }
                assert containingType != null;
                Set<CtAnnotation<Pattern>> patternAnnotations = util.getAllPatternAnnotations(containingType);
                boolean hasCorrectAnnotation = memberRoles.isEmpty();
                for (Iterator<CtAnnotation<Member>> iterator = memberRoles.iterator(); !hasCorrectAnnotation && iterator.hasNext(); ) {
                    CtAnnotation<Member> memberRole = iterator.next();
                    String id = Member.Fields.val(Member.Fields.ID,memberRole);
                    PatternInstance patternInstance = util.getPatternInstance(id);
                    DesignPattern pattern = patternInstance.getName();
                    String patternIdentifier = pattern.getIdentifier();
                    MemberRole role = pattern.getMember(Member.Fields.val(Member.Fields.ROLE, memberRole));
                    Set<CallSite> callSites = role.getCallSites();
                    boolean correct = callSites.isEmpty();
                    for (CallSite callSite : callSites) {
                        String roleName = callSite.getType().getName();
                        for (CtAnnotation<Pattern> patternAnnotation : patternAnnotations) {
                            boolean correctId = Pattern.Fields.val(Pattern.Fields.ID, patternAnnotation).equals(id);
                            boolean correctName = Pattern.Fields.val(Pattern.Fields.NAME,patternAnnotation).equals(patternIdentifier);
                            String actualRole = Pattern.Fields.val(Pattern.Fields.ROLE, patternAnnotation);
                            boolean correctRole = actualRole.equals(roleName);
                            correct = correct || (correctId && correctName && correctRole);

                            //Check the inheritance stack
                            //Check both for calls in inheritors as well as calls from the same role (implementational detail)
                            if (!correct && correctId && correctName) {
                                Queue<TypeRole> parentTypeRoles = new LinkedList<TypeRole>();
                                TypeRole typeRole = pattern.getTypeRole(actualRole);
                                parentTypeRoles.add(typeRole);
                                while (!correct && (typeRole = parentTypeRoles.poll()) != null) {
                                    Collection<TypeRole> parentRoles = typeRole.getParentRoles();
                                    parentTypeRoles.addAll(parentRoles);
                                    correct = typeRole.getName().equals(roleName) ||
                                            typeRole.getMembers().containsValue(role); //Also valid if we're subclassing the class where the method was declared

                                }
                            }
                        }

                        //todo: Missing check for correct method
                    }
                    hasCorrectAnnotation = correct;
                }
                if (!hasCorrectAnnotation) {
                    getEnvironment().report(CallSiteProcessor.this, Severity.ERROR, containingExecutable, "Illegal call site for " + e);
                }


                for (CtExecutableReference<?> executableReference : methodRefs.keySet()) {
                    boolean b = executableReference.isOverriding(ctExecutableReference);
                    if (b) {
                        CtMethod<?> ctMethod = methodRefs.get(executableReference);
                        mapPut(referencingTypes, ctMethod, new CtCallSite(containingType, containingExecutable));
                    }
                }
            }
        }

    }
}
